/**
 * Copyright (c) 2010, Todd Ginsberg
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * Neither the name of Todd Ginsberg, or Gowalla nor the
 *      names of any contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 *  Also, please use this for Good and not Evil.  
 */
package com.ginsberg.gowalla;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.ginsberg.gowalla.auth.AnonymousAuthentication;
import com.ginsberg.gowalla.auth.Authentication;
import com.ginsberg.gowalla.dto.Category;
import com.ginsberg.gowalla.dto.Checkin;
import com.ginsberg.gowalla.dto.CheckinReceipt;
import com.ginsberg.gowalla.dto.FullCategory;
import com.ginsberg.gowalla.dto.FullSpot;
import com.ginsberg.gowalla.dto.FullUser;
import com.ginsberg.gowalla.dto.GeoPoint;
import com.ginsberg.gowalla.dto.Highlight;
import com.ginsberg.gowalla.dto.Id;
import com.ginsberg.gowalla.dto.Item;
import com.ginsberg.gowalla.dto.ItemEvent;
import com.ginsberg.gowalla.dto.Locatable;
import com.ginsberg.gowalla.dto.Pin;
import com.ginsberg.gowalla.dto.PinRequestStatus;
import com.ginsberg.gowalla.dto.PinRequestType;
import com.ginsberg.gowalla.dto.Problem;
import com.ginsberg.gowalla.dto.SimpleSpot;
import com.ginsberg.gowalla.dto.Spot;
import com.ginsberg.gowalla.dto.SpotEvent;
import com.ginsberg.gowalla.dto.SpotPhoto;
import com.ginsberg.gowalla.dto.Stamp;
import com.ginsberg.gowalla.dto.Trip;
import com.ginsberg.gowalla.dto.TripSummary;
import com.ginsberg.gowalla.dto.User;
import com.ginsberg.gowalla.dto.UserPhoto;
import com.ginsberg.gowalla.dto.VisitedSpot;
import com.ginsberg.gowalla.dto.problems.ProblemReport;
import com.ginsberg.gowalla.exception.GowallaException;
import com.ginsberg.gowalla.exception.GowallaRequestException;
import com.ginsberg.gowalla.exception.RateLimitExceededException;
import com.ginsberg.gowalla.exception.RequestNotAcceptableException;
import com.ginsberg.gowalla.exception.RequestNotFoundException;
import com.ginsberg.gowalla.exception.ServiceUnavailableException;
import com.ginsberg.gowalla.rate.RateLimiter;
import com.ginsberg.gowalla.rate.RequestsOverTime;
import com.ginsberg.gowalla.request.DefaultRequestHandler;
import com.ginsberg.gowalla.request.PlainRequestHeader;
import com.ginsberg.gowalla.request.RequestHandler;
import com.ginsberg.gowalla.request.RequestHeader;
import com.ginsberg.gowalla.request.RequestParameter;
import com.ginsberg.gowalla.request.StringRequestParameter;
import com.ginsberg.gowalla.request.translate.GsonResponseTranslator;
import com.ginsberg.gowalla.request.translate.ResponseTranslator;
import com.ginsberg.gowalla.util.Lists;

/**
 * This is the main class used to contact the Gowalla servers.  It takes
 * care of formulating the request, parsing the response, authenticating
 * the user, presenting an API key, and limiting the rate at which requests
 * are made.  
 * 
 * Example (Anonymous):
 * <pre>
 * {@code
 * // Get the author's favorite spot.
 * Gowalla gowalla = new Gowalla("Testing");
 * System.out.println(gowalla.getSpot(11888));
 * }
 * </pre>
 * 
 * Example (Authenticated):
 * <pre>
 * {@code
 * // Get the author's user record.
 * Gowalla gowalla = new Gowalla("Testing", new BasicAuthentication("username", "Password", "YOUR_API_KEY"));
 * System.out.println(gowalla.getStamps("tginsberg"));
 * }
 * </pre> 
 * 
 * @author Todd Ginsberg
 * @author Ryan Crutchfield - rc6750@gmail.com
 *
 */
public class Gowalla {

	protected enum RequestMethod {GET,POST};
	protected RateLimiter rateLimiter = null;
	protected ResponseTranslator responseTranslator = new GsonResponseTranslator();
	protected RequestHandler handler = null;
	protected Authentication authentication = null;
	protected Set<RequestHeader> perRequestHeaders = new HashSet<RequestHeader>();
	
	/**
	 * Create an instance of the Gowalla Request object, using anonymous authentication.
	 * @param applicationName The Name of the application you are writing - requred by Gowalla.
	 */
	public Gowalla(final String applicationName) {
		this(applicationName, null, new AnonymousAuthentication());
	}
	
	/**
	 * Create an instance of the Gowalla Request object, using anonymous authentication.
	 * @param applicationName The Name of the application you are writing - requred by Gowalla.
	 * @param apiKey Recommended by Gowalla, available at gowalla.com/api, provide this to get a higher request rate.
	 */
	public Gowalla(final String applicationName, final String apiKey) {
		this(applicationName, apiKey, new AnonymousAuthentication());
	}
	
	/**
	 * Create an instance of the Gowalla Request object, with a specific type of authentication.
	 * @param applicationName The Name of the application you are writing - requred by Gowalla.
	 * @param authentication The authentication scheme you wish to use.  
	 */
	public Gowalla(final String applicationName, final Authentication authentication) {
		this(applicationName, null, authentication);	
	}
	
	/**
	 * Create an instance of the Gowalla Request object, with a specific type of authentication.
	 * @param applicationName The Name of the application you are writing - requred by Gowalla.
	 * @param apiKey Recommended by Gowalla, available at gowalla.com/api, provide this to get a higher request rate.
	 * @param authentication The authentication scheme you wish to use.  
	 */
	public Gowalla(final String applicationName, final String apiKey, final Authentication authentication) {
		super();
		setAuthentication(authentication);
		perRequestHeaders.add(new PlainRequestHeader("User-Agent", String.format("%s (%s)", applicationName, Version.getVersion())));
		perRequestHeaders.add(new PlainRequestHeader("Accept", "application/json"));  // Try others for fun?
		if(apiKey != null) {
			perRequestHeaders.add(new PlainRequestHeader("X-Gowalla-API-Key", apiKey));
		}	
		setRequestHandler(new DefaultRequestHandler());		
		
		// Set the rate.
		setRateLimiter(new RequestsOverTime(apiKey == null ? 1 : 5));
	}
	
	/**
	 * Set the Authentication scheme for subsequent requests.
	 */
	public void setAuthentication(final Authentication authentication) {
		this.authentication = authentication;
	}
		
	/**
	 * Set a new type of request handler. 
	 * @param handler A non-null request handler.
	 */
	public void setRequestHandler(final RequestHandler handler) {
		this.handler = handler;
		this.handler.setRequestHeaders(perRequestHeaders);
	}
	
	/**
	 * Return a live reference to the request handler this object is using.
	 */
	public RequestHandler getRequestHandler() {
		return handler;
	}
	
	/**
	 * @return the rateLimiter
	 */
	public RateLimiter getRateLimiter() {
		return rateLimiter;
	}

	/**
	 * @param rateLimiter the rateLimiter to set
	 */
	public void setRateLimiter(final RateLimiter rateLimiter) {
		this.rateLimiter = rateLimiter;
	}
	
	/**
	 * Check the currently authenticated user into the spot given.  Please see
	 * Gowalla's guidelines before making any calls to this method.
	 * 
	 * Also, please USE THE TEST MODE when testing your code.  This will
	 * do the same logic on Gowalla's end, it just won't record you checking in,
	 * publish to twitter or facebook, and presumably won't award items or pins.
	 * 
	 * Guidelines:
	 * http://gowalla.com/api/docs/checkin#guidelines_for_using_the_check_in_api
	 * 
	 * @param spotId The identity of the spot to check the user into.
	 * @param point The point the user is standing on, which you received from a GPS device.
	 * @param comment The comment the user wishes to attach (if any) to their checkin.
	 * @param twitter Should the checkin be announced on Twitter? Make sure you ask first.
	 * @param facebook Should the checkin be annoucned on Facebook? Make sure you ask first.
	 * @param testing Is this test mode? 
	 * @throws GowallaException
	 */
	public CheckinReceipt checkin(final Id<Spot> spotId, final GeoPoint point, final String comment, boolean twitter, boolean facebook, boolean testing) throws GowallaException {
		return checkin(spotId.getId(), point, comment, twitter, facebook, testing); 
	}
	
	/**
	 * Check the currently authenticated user into the spot given.  Please see
	 * Gowalla's guidelines before making any calls to this method.
	 * 
	 * Also, please USE THE TEST MODE when testing your code.  This will
	 * do the same logic on Gowalla's end, it just won't record you checking in,
	 * publish to twitter or facebook, and presumably won't award items or pins.
	 * 
	 * Guidelines:
	 * http://gowalla.com/api/docs/checkin#guidelines_for_using_the_check_in_api
	 * 
	 * @param spotId The identity of the spot to check the user into.
	 * @param point The point the user is standing on, which you received from a GPS device.
	 * @param comment The comment the user wishes to attach (if any) to their checkin.
	 * @param twitter Should the checkin be announced on Twitter? Make sure you ask first.
	 * @param facebook Should the checkin be announced on Facebook? Make sure you ask first.
	 * @param testing Is this test mode? 
	 * @throws GowallaException
	 */
	public CheckinReceipt checkin(final int spotId, final GeoPoint point, final String comment, boolean twitter, boolean facebook, boolean testing) throws GowallaException {
		final String resource = "/checkins" + (testing ? "/test" : "");
		final Collection<RequestParameter> parameters = new LinkedList<RequestParameter>();
		parameters.add(new StringRequestParameter("spot_id", spotId));
		parameters.add(new StringRequestParameter("comment", comment));
		parameters.add(new StringRequestParameter("lat", point.getLatitude()));
		parameters.add(new StringRequestParameter("lnt", point.getLongitude()));
		parameters.add(new StringRequestParameter("post_to_twitter", twitter ? 1 : 0));
		parameters.add(new StringRequestParameter("post_to_facebook", facebook ? 1 : 0));
		parameters.add(new StringRequestParameter("json_only", "true"));
		
		String resp = post(resource, parameters);
		return responseTranslator.translateCheckinReceipt(resp);
	}
	
	/**
	 * Comment on the given checkin as the currently authenticated user.
	 * 
	 * See http://gowalla.com/api/docs/checkin#posting_comments for Guidelines.
	 * 
	 * @param checkinId The id of the checkin to comment on.
	 * @param message The comment the user wishes to make.
	 * @param twitter Should the comment be announced on Twitter? Make sure you ask first.
	 * @param facebook Should the comment be announced on Facebook? Make sure you ask first.
	 * @throws GowallaException
	 */
	public void commentOnCheckin(final int checkinId, final String message, boolean twitter, boolean facebook) throws GowallaException {
		final String resource = String.format("/checkins/%d/activity", checkinId);
		final Collection<RequestParameter> parameters = new LinkedList<RequestParameter>();
		parameters.add(new StringRequestParameter("message", message));
		parameters.add(new StringRequestParameter("post_to_twitter", twitter ? 1 : 0));
		parameters.add(new StringRequestParameter("post_to_facebook", facebook ? 1 : 0));
		
		post(resource, parameters);
	}
	
	/**
	 * Comment on the given checkin as the currently authenticated user.
	 * 
	 * See http://gowalla.com/api/docs/checkin#posting_comments for Guidelines.
	 * 
	 * @param checkinId The identity of the checkin to comment on.
	 * @param message The comment the user wishes to make.
	 * @param twitter Should the comment be announced on Twitter? Make sure you ask first.
	 * @param facebook Should the comment be announced on Facebook? Make sure you ask first.
	 * @throws GowallaException
	 */
	public void commentOnCheckin(final Id<Checkin> checkinId, final String message, boolean twitter, boolean facebook) throws GowallaException {
		commentOnCheckin(checkinId.getId(), message, twitter, facebook);
	}
	
	/**
	 * Find spots according to the criteria given.  If no spots are found, an empty list
	 * is returned.
	 * 
	 * Functional note: Gowalla doesn't return full spot information with this call, so 
	 * SimpleSpot objects are returned instead, so as not to mislead the caller into
	 * thinking some data isn't available.
	 * 
	 * WARNING: Depending on your critiera this method may use calls not officially 
	 * supported by Gowalla. This means it may go away without warning while you are 
	 * using it.  It also means we may have to drop support for it.
	 * 	  
	 * @param criteria A SpotCriteria Object.
	 * @return A List of SimpleSpots.
	 * @see Dr. Barnabus Peddingferd
	 * @throws GowallaException
	 */
	public List<SimpleSpot> findSpots(final SpotCriteria criteria) throws GowallaException {
		if(criteria == null) {
			throw new GowallaException("No Critiera provided.");
		}
		// Store these in a Set because Gowalla's paging sometimes returns duplicates.
		Set<SimpleSpot> spotsReturned = new LinkedHashSet<SimpleSpot>(); 
		boolean keepGoing = true;
		int spotsLastRequest = 0;
		int attempts = 0;
		
		while(keepGoing) {
			try {
				attempts++;
				final String response = get("/spots", criteria.createRequestParameters(spotsLastRequest));
				attempts = 0;
				spotsReturned.addAll(responseTranslator.translateSimpleSpots(response));

				// Don't keep paging if we don't support it, are over the limit, or didn't receive anything.
				if(criteria.getPagingSupport() == PagingSupport.SINGLE_REQUEST_ONLY ||
						(criteria.getNumberOfSpots() != 0 && spotsReturned.size() >= criteria.getNumberOfSpots())  ||
						spotsReturned.size() == spotsLastRequest) {
					keepGoing = false;
				} else {
					spotsLastRequest = spotsReturned.size();
				}
			} catch(ServiceUnavailableException e) {
				// See if we can retry this or not.
				if(attempts >= criteria.getRetries()+1) {
					// We are out of attempts.
					throw e;
				}
			}
		}
		List<SimpleSpot> toBeReturned = new LinkedList<SimpleSpot>(spotsReturned);
		
		// Sorting, if supported.
		if(criteria.getSortBy() != null) {
			Collections.sort(toBeReturned, criteria.getSortBy());
		}
		
		// Filtering, if supported.
		if(criteria.getFilter() != null) {
			for(Iterator<SimpleSpot> i = toBeReturned.iterator(); i.hasNext();) {
				if(!criteria.getFilter().isIncluded(i.next())) {
					i.remove();
				}
			}
		}
		
		if(toBeReturned.size() > criteria.getNumberOfSpots() && criteria.getNumberOfSpots() != 0) {
			// Do it this way because subList is still backed by the larger list.
			toBeReturned = new LinkedList<SimpleSpot>(toBeReturned.subList(0, criteria.getNumberOfSpots()));
		}
		return toBeReturned;

	}
	
	/**
	 * Find spots within the radius given.  If no spots exist within the radius given, an empty
	 * list will be returned.  The spots will be returned in order of nearest to farthest from
	 * the spot given.
	 * 
	 * Functional note: Gowalla seems to truncate the results to 40 spots.
	 * 
	 * Functional note: Gowalla doesn't return full spot information with this call, so 
	 * SimpleSpot objects are returned instead, so as not to mislead the caller into
	 * thinking some data isn't available.
	 * 
	 * @param location An object expressing the point to search from.
	 * @param radiusMeters How far away from the spot to search.  If negative, the absolute value is taken.
	 * @return A List of SimpleSpots.
	 * @throws GowallaException
	 */
	public List<SimpleSpot> findSpotsNear(final Locatable location, final int radiusMeters) throws GowallaException  {
		return findSpots(new SpotCriteria.Builder(location, radiusMeters).build());
	}
	
	/**
	 * Find spots that match the search phrase near the given location. If no spots exist within the radius given, an empty
	 * list will be returned.  
	 * 
	 * Functional note: Gowalla seems to truncate the results to 40 spots.
	 * 
	 * Functional note: Gowalla doesn't return full spot information with this call, so 
	 * SimpleSpot objects are returned instead, so as not to mislead the caller into
	 * thinking some data isn't available.
	 * 
	 * @param location A string representing a location (Tampa, 33607, Austin, TX, etc).
	 * @param searchString search string (coffee, bar, monkey).
	 * @return A List of SimpleSpots.
	 * @throws GowallaException
	 */
	public List<SimpleSpot> searchSpots(final String location, final String searchString) throws GowallaException  {
		final String response = get(String.format("/spots",searchString), 
				new StringRequestParameter("l", location), 
				new StringRequestParameter("q", searchString));
		final List<SimpleSpot> spots = responseTranslator.translateSimpleSpots(response);
		return spots;
	}
	
	/**
	 * Get the data we know about a specific numbered checkin.
	 * 
	 * @param checkinId The ID of the checkin you are interested in.
	 * @return A checkin object or null if it doesn't exist.
	 * @throws GowallaException when something goes wrong.
	 */
	public Checkin getCheckin(final Id<Checkin> checkinId) throws GowallaException {
		return getCheckin(checkinId.getId());
	}
	
	/**
	 * Get the data we know about a specific numbered checkin.
	 * 
	 * @param id The ID of the checkin you are interested in.
	 * @return A checkin object or null if it doesn't exist.
	 * @throws GowallaException when something goes wrong.
	 */
	public Checkin getCheckin(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/checkins/%d", id));
			return responseTranslator.translateCheckin(response);
		} catch(RequestNotAcceptableException e) {
			// No checkin for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No checkin for this number.
			return null;			
		}
	}
	
	/**
	 * Get a list of all the Categories, nested.
	 * @throws GowallaException when we cannot connect, parse results, or authenticate.
	 */
	public List<FullCategory> getCategories() throws GowallaException {
		final String resp = get("/categories");
		return responseTranslator.translateCategories(resp);
	}
	
	/**
	 * Get a specific Category.
	 * @throws GowallaException when we cannot connect, parse results, or authenticate.
	 */
	public FullCategory getCategory(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/categories/%d", id));
			return responseTranslator.translateCategory(response);
		} catch(RequestNotAcceptableException e) {
			// No category for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No category for this number.
			return null;			
		}
	}
	
	/**
	 * Get a specific Category.
	 * @throws GowallaException when we cannot connect, parse results, or authenticate.
	 */
	public FullCategory getCategory(final Id<Category> identity) throws GowallaException {
		return getCategory(identity.getId());
	}
	
	/**
	 * Get the full Spot data for a given spot id.
	 * 
	 * @param id
	 * @return A Spot object, or null if no spot with that id exists.
	 * @throws GowallaException
	 */
	public FullSpot getSpot(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/spots/%d", id));
			return responseTranslator.translateSpot(response, id);
		}catch(RequestNotAcceptableException e) {
			// No Spot for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No spot for this number.
			return null;			
		}
	}
	
	/**
	 * Get the full Spot data for a given spot id.
	 * 
	 * @param identity
	 * @return A Spot object, or null if no spot with that id exists.
	 * @throws GowallaException
	 */
	public FullSpot getSpot(final Id<Spot> identity) throws GowallaException {
		return getSpot(identity.getId());
	}
	
	/**
	 * Get items the user is carrying, missing, or has in their vault.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param identity The user identity.
	 * @param context Which type of items are being requested.
	 * @throws GowallaException
	 */
	public List<Item> getItemsForUser(final Id<User> identity, final ItemContext context) throws GowallaException {
		return getItemsForUser(identity.getId(), context);
	}
	
	/**
	 * Get items the user is carrying, missing, or has in their vault.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param id The user number.
	 * @param context Which type of items are being requested.
	 * @throws GowallaException
	 */
	public List<Item> getItemsForUser(final int id, final ItemContext context) throws GowallaException {
		return getItemsForUserWithPaging(String.format("/users/%d/items/%s", id, context.getUrl()));
	}
	
	/**
	 * Get items the user is carrying, missing, or has in their vault.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param username The username of the user you are requesting items for.
	 * @param context Which type of items are being requested.
	 * @throws GowallaException
	 */
	public List<Item> getItemsForUser(final String username, final ItemContext context) throws GowallaException {
		return getItemsForUserWithPaging(String.format("/users/%s/items/%s", username, context.getUrl()));
	}
	
	/**
	 * Internal call that supports forced paging to get all items, going on the theory
	 * that not knowing them all isn't entirely useful.  
	 * 
	 * TODO: Wait on Gowalla to implement general purpose paging here.
	 */
	private List<Item> getItemsForUserWithPaging(final String resource, final RequestParameter... params) throws GowallaException {
		final List<Item> items = new LinkedList<Item>();
		int page = 1;
		int returned = 0;

		try {
			do {
				returned = items.size();
				HashSet<RequestParameter> paramSet = new HashSet<RequestParameter>();
				Collections.addAll(paramSet, params);
				paramSet.add(new StringRequestParameter("page", page++));
				final String response = get(resource, paramSet);
				items.addAll(responseTranslator.translateItems(response));
			} while(items.size() != returned);
			return items;
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		}catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * Get a user's friends.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param id The user number.
	 * @throws GowallaException
	 */
	public List<User> getUserFriends(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d/friends",id));
			return responseTranslator.translateUsers(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * Get a user's friends.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param identity The user number.
	 * @throws GowallaException
	 */
	public List<User> getUserFriends(final Id<User> identity) throws GowallaException {
		return getUserFriends(identity.getId());
	}
	
	/**
	 * Get a user's friends.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param username The username of the user you are requesting friends of.
	 * @throws GowallaException
	 */
	public List<User> getUserFriends(final String username) throws GowallaException {
		try {
			final String response = get(String.format("/users/%s/friends",username));
			return responseTranslator.translateUsers(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this name.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * Get highlights for a specific spot, by identity.
	 * 
	 * @param identity A non-null identity object.
	 * @return null on error, empty list when no highlights, or a populated list of Highlight objects.
	 * @throws GowallaException
	 */
	public List<Highlight> getSpotHighlights(final Id<Spot> identity) throws GowallaException {
		return getSpotHighlights(identity.getId());
	}
	
	/**
	 * Get highlights for a specific user, by id.
	 * 
	 * @param id A non-null identity object.
	 * @return null on error, empty list when no highlights, or a populated list of Highlight objects.
	 * @throws GowallaException
	 */
	public List<Highlight> getSpotHighlights(final int id) throws GowallaException {
		return pageRequests(String.format("/spots/%d/highlights", id), 
				new PagedDataHandler<Highlight>() {		
					@Override
					public int maxResults() {
						return 0;
					}
					@Override
					public boolean canPage() {
						return true;
					}
					@Override
					public List<Highlight> translate(String rawData) {
						return responseTranslator.translateHighlights(rawData);
					}
			});
	}
	
	
	/**
	 * Get highlights for a specific user, by identity.
	 * 
	 * @param identity A non-null identity object.
	 * @return null on error, empty list when no highlights, or a populated list of Highlight objects.
	 * @throws GowallaException
	 */
	public List<Highlight> getUserHighlights(final Id<User> identity) throws GowallaException {
		return getUserHighlights(identity.getId());
	}
	
	/**
	 * Get highlights for a specific user, by id.
	 * 
	 * @param id A non-null identity object.
	 * @return null on error, empty list when no highlights, or a populated list of Highlight objects.
	 * @throws GowallaException
	 */
	public List<Highlight> getUserHighlights(final int id) throws GowallaException {
		return pageRequests(String.format("/users/%d/highlights", id), 
				new PagedDataHandler<Highlight>() {		
					@Override
					public int maxResults() {
						return 0;
					}
					@Override
					public boolean canPage() {
						return true;
					}
					@Override
					public List<Highlight> translate(String rawData) {
						return responseTranslator.translateHighlights(rawData);
					}
				});
	}
	
	/**
	 * Get highlights for a specific user, by username.
	 * 
	 * @param username The username of the user you want hightlights for.
	 * @return null on error, empty list when no highlights, or a populated list of Highlight objects.
	 * @throws GowallaException
	 */
	public List<Highlight> getUserHighlights(final String username) throws GowallaException {
		return pageRequests(String.format("/users/%s/highlights", username), 
				new PagedDataHandler<Highlight>() {		
					@Override
					public int maxResults() {
						return 0;
					}
					@Override
					public boolean canPage() {
						return true;
					}
					@Override
					public List<Highlight> translate(String rawData) {
						return responseTranslator.translateHighlights(rawData);
					}
				});
	}
	/**
	 * Get a user's pins.
	 *
	 * @param id The user number.
	 * @throws GowallaException
	 */
	public List<Pin> getUserPins(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d/pins",id));
			return responseTranslator.translateUserPins(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
		
	/**
	 * Get a user's pins.
	 * 
	 * @param identity The user number.
	 * @throws GowallaException
	 */
	public List<Pin> getUserPins(final Id<User> identity) throws GowallaException {
		return getUserPins(identity.getId());
	}
	
	/**
	 * Get a user's pins.
	 * 
	 * @param username The username of the user you are requesting pins of.
	 * @throws GowallaException
	 */
	public List<Pin> getUserPins(final String username) throws GowallaException {
		try {
			final String response = get(String.format("/users/%s/pins",username));
			return responseTranslator.translateUserPins(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this name.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * Get a user's pins, filtered by type (challenge, region, etc) and status (if they've received the pin yet or not).
	 *
	 * @param id The user number.
	 * @param type The type of pins to return.
	 * @param status The status of the pins to return.
	 * @throws GowallaException
	 */
	public List<Pin> getUserPins(final int id, final PinRequestType type, final PinRequestStatus status) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d/pins/%s%s",id, type.getResource(), status.getResource()));
			return responseTranslator.translateUserPins(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
		
	/**
	 * Get a user's pins, filtered by type (challenge, region, etc) and status (if they've received the pin yet or not).
	 * 
	 * @param identity The user number.
	 * @param type The type of pins to return.
	 * @param status The status of the pins to return.
	 * @throws GowallaException
	 */
	public List<Pin> getUserPins(final Id<User> identity, final PinRequestType type, final PinRequestStatus status) throws GowallaException {
		return getUserPins(identity.getId(), type, status);
	}
	
	/**
	 * Get a user's pins, filtered by type (challenge, region, etc) and status (if they've received the pin yet or not).
	 * 
	 * @param username The username of the user you are requesting pins of.
	 * @param type The type of pins to return.
	 * @param status The status of the pins to return.
	 * @throws GowallaException
	 */
	public List<Pin> getUserPins(final String username, final PinRequestType type, final PinRequestStatus status) throws GowallaException {
		try {
			final String response = get(String.format("/users/%s/pins%s%s", username, type.getResource(), status.getResource()));
			return responseTranslator.translateUserPins(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this name.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * Get a user's created trips
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param id The user number.
	 * @throws GowallaException
	 */
	public List<TripSummary> getUserCreatedTrips(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d/trips",id));
			return responseTranslator.translateUserCreatedTrips(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * Get a user's created trips
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param username The username of the user you are requesting trips created by.
	 * @throws GowallaException
	 */
	public List<TripSummary> getUserCreatedTrips(final String username) throws GowallaException {
		try {
			final String response = get(String.format("/users/%s/trips",username));
			return responseTranslator.translateUserCreatedTrips(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this name.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * Get the list of photos that a user has posted.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param id The int id of the user.
	 * @return A list of photos, or null if no such user exists.
	 * @throws GowallaException
	 */
	public List<UserPhoto> getUserPhotos(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d/photos", id));
			return responseTranslator.translateUserPhotos(response);
		} catch(RequestNotAcceptableException e) {
			// No Spot for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Spot for this number.
			return null;			
		}	
	}
	
	/**
	 * Get the list of photos that a user has posted.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param identity The id of the user.
	 * @return A list of photos, or null if no such user exists.
	 * @throws GowallaException
	 */
	public List<UserPhoto> getUserPhotos(final Id<User> identity) throws GowallaException {
		return getUserPhotos(identity.getId());
	}
		
	/**
	 * Get all of the items at a spot.  
	 * 
	 * @param id The id of the spot.
	 * @return a List of Items, an empty list for a valid spot with no items, null if spot is invalid.
	 */
	public List<Item> getItemsAtSpot(int id) throws GowallaException {
		try {
			final String response = get(String.format("/spots/%d/items", id));
			return responseTranslator.translateItems(response);
		}catch(RequestNotAcceptableException e) {
			// No Spot for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Spot for this number.
			return null;			
		}
	}
	
	/**
	 * Get all of the items at a spot.  
	 * 
	 * @param identity The id of the spot.
	 * @return a List of Items, an empty list for a valid spot with no items, null if spot is invalid.
	 */
	public List<Item> getItemsAtSpot(final Id<Spot> identity) throws GowallaException {
		return getItemsAtSpot(identity.getId());
	}
	
	/**
	 * Get an item, by id.
	 */
	public Item getItem(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/items/%d", id));
			return responseTranslator.translateItem(response);
		} catch(RequestNotAcceptableException e) {
			// No Item for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Item for this number.
			return null;			
		}	
	}
	
	/**
	 * Get an item, by id.
	 */
	public Item getItem(final Id<Item> identity) throws GowallaException {
		return getItem(identity.getId());
	}
	
	/**
	 * Get the list of events that happened at a spot.
	 * 
	 * @param identity The id of the spot.
	 * @return A list of events, or null if no such spot exists.
	 * @throws GowallaException
	 */
	public List<SpotEvent> getSpotEvents(final Id<Spot> identity) throws GowallaException {
		return getSpotEvents(identity.getId());
	}
	
	/**
	 * Get the list of recent events that happened at a spot.
	 * 
	 * @param id The int id of the spot.
	 * @return A list of events, or null if no such spot exists.
	 * @throws GowallaException
	 */
	public List<SpotEvent> getSpotEvents(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/spots/%d/events", id));
			return responseTranslator.translateSpotEvents(response);
		} catch(RequestNotAcceptableException e) {
			// No Spot for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Spot for this number.
			return null;			
		}	
	}
	
	/**
	 * Get the list of photos that happened at a spot.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param id The int id of the spot.
	 * @return A list of photos, or null if no such spot exists.
	 * @throws GowallaException
	 */
	public List<SpotPhoto> getSpotPhotos(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/spots/%d/photos", id));
			return responseTranslator.translateSpotPhotos(response);
		} catch(RequestNotAcceptableException e) {
			// No Spot for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Spot for this number.
			return null;			
		}	
	}
	
	/**
	 * Get the list of photos that happened at a spot.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param identity The id of the spot.
	 * @return A list of photos, or null if no such spot exists.
	 * @throws GowallaException
	 */
	public List<SpotPhoto> getSpotPhotos(final Id<Spot> identity) throws GowallaException {
		return getSpotPhotos(identity.getId());
	}
	
	/**
	 * Find all active flags at a spot, by spot id.
	 * 
	 * @param identity The id of the spot in question.
	 * @return A list of problems, or an empty list if no problems.
	 * @throws GowallaException
	 */
	public List<Problem> getSpotProblems(final Id<Spot> identity) throws GowallaException {
		return getSpotProblems(identity.getId());
	}
	
	/**
	 * Find all active flags at a spot, by spot id.
	 * 
	 * @param id The id of the spot in question.
	 * @return A list of problems, or an empty list if no problems.
	 * @throws GowallaException
	 */
	public List<Problem> getSpotProblems(final int id) throws GowallaException {
		return pageRequests(String.format("/spots/%d/flags", id), 
				new PagedDataHandler<Problem>() {		
					@Override
					public int maxResults() {
						return 0;
					}
					@Override
					public boolean canPage() {
						return true;
					}
					@Override
					public List<Problem> translate(String rawData) {
						return responseTranslator.translateProblems(rawData);
					}
				});
	}
	
	/**
	 * Find all active flags.  Call with 0 for no limit or use
	 * no-argument version of this method.
	 * 
	 * @param max The max number of problems to return.
	 * @return A list of problems, or an empty list if no problems.
	 * @throws GowallaException
	 */
	public List<Problem> getProblems(final int max) throws GowallaException {
		return pageRequests("/flags", 
				new PagedDataHandler<Problem>() {		
					@Override
					public int maxResults() {
						return max;
					}
					@Override
					public boolean canPage() {
						return max == 0;
					}

					@Override
					public List<Problem> translate(String rawData) {
						return responseTranslator.translateProblems(rawData);
					}
				});
	}
	
	/**
	 * Find all active flags.
	 * 
	 * @return A list of problems, or an empty list if no problems.
	 * @throws GowallaException
	 */
	public List<Problem> getProblems() throws GowallaException {
		return getProblems(0);
	}
	
	/**
	 * Report a Problem to Gowalla.  Please see ProblemFactory for creating ProblemReport objects.
	 * 
	 * @param report Required report object.
	 * @return The Problem reported.
	 * @throws GowallaException
	 * @see com.ginsberg.gowalla.dto.problems.ProblemFactory
	 */
	public Problem reportSpotProblem(final ProblemReport report) throws GowallaException {
		final String response = post(String.format("/spots/%d/flags/%s", report.getSpotId(), report.getType().name().toLowerCase()),
				new StringRequestParameter("description", report.getDescription()));
		return responseTranslator.translateProblem(response);
	}
		
	/**
	 * Get the list of events that happened to an item.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 *  
	 * @param id The int id of the item.
	 * @return A list of events, or null if no such spot exists.
	 * @throws GowallaException
	 */
	public List<ItemEvent> getItemEvents(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/items/%d/events", id));
			return responseTranslator.translateItemEvents(response);
		} catch(RequestNotAcceptableException e) {
			// No Spot for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Spot for this number.
			return null;			
		}	
	}
	
	/**
	 * Get the list of events that happened to an item.
	 * 
	 * WARNING: This method uses calls not officially supported by Gowalla.  This 
	 * means it may go away without warning while you are using it.  It also means
	 * we may have to drop support for it.
	 * 
	 * @param identity The id of the item.
	 * @return A list of events, or null if no such spot exists.
	 * @throws GowallaException
	 */
	public List<ItemEvent> getItemEvents(final Id<Item> identity) throws GowallaException {
		return getItemEvents(identity.getId());
	}

	/**
	 * Get a Trip, by id.  If the trip doesn't exist, this will return null.
	 * 
	 * @param identity The id of the trip you want.
	 * @return a Trip object or null if not found.
	 * @throws GowallaException
	 */
	public Trip getTrip(final Id<Trip> identity) throws GowallaException {
		return getTrip(identity.getId());
	}
	
	/**
	 * Get a Trip, by id.  If the trip doesn't exist, this will return null.
	 * 
	 * @param id The integer id of the trip you want.
	 * @return a Trip object or null if not found.
	 * @throws GowallaException
	 */
	public Trip getTrip(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/trips/%d", id));
			return responseTranslator.translateTrip(response);
		} catch(RequestNotAcceptableException e) {
			// No Trip for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Trip for this number.
			return null;			
		}	
	}
	
	/**
	 * Get all of the trips the currently authenticated user is allowed to see, in summary form.
	 * @throws GowallaException
	 */
	public List<TripSummary> getTrips() throws GowallaException {
		try {
			final String response = get("/trips");
			return responseTranslator.translateTripSummaries(response);
		} catch(RequestNotAcceptableException e) {
			// No Trip for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No Trip for this number.
			return null;			
		}	
	}
	
	/**
	 * Get a list of stamps for a specific user, by login.  
	 * 
	 * @param criteria A critiera object that defines how to locate the stamps.
	 * 
	 * @return A list of Stamps, or null if the user doesn't exist.
	 * @throws GowallaException
	 */
	public List<Stamp> getStamps(final StampCriteria criteria) throws GowallaException {
		return pageRequests(criteria.getResource(), 
				new PagedDataHandler<Stamp>() {
					@Override
					public int maxResults() {
						return criteria.getLimit();
					}
					@Override
					public boolean canPage() {
						return criteria.getPagingSupport() == PagingSupport.PAGING_ALLOWED;
					}
					@Override
					public List<Stamp> translate(String rawData) {
						return responseTranslator.translateStamps(rawData);
					}
			},
			criteria.getParameters());
	}
		
	/**
	 * Get a list of stamps for a specific user, by username, paging (multiple server calls) if needed
	 * in order to get them all.  If you want to limit it, please use the StampCriteria version of 
	 * this call and turn paging off or set a limit.
	 * 
	 * @param username The username name of the user.
	 * 
	 * @return A list of Stamps, or null if the user doesn't exist.
	 * @throws GowallaException
	 */
	public List<Stamp> getStamps(final String username) throws GowallaException {
		return getStamps(new StampCriteria.Builder(username).build());
	}
		
	/**
	 * Get a list of stamps for a specific user, by id, paging (multiple server calls) if needed
	 * in order to get them all.  If you want to limit it, please use the StampCriteria version of 
	 * this call and turn paging off or set a limit.
	 * 
	 * @param id The id name of the user.
	 * 
	 * @return A list of Stamps, or null if the user doesn't exist.
	 * @throws GowallaException
	 */
	public List<Stamp> getStamps(final int id) throws GowallaException {
		return getStamps(new StampCriteria.Builder(id).build());
	}
	
	/**
	 * Get a list of stamps for a specific user, by identity, paging (multiple server calls) if needed
	 * in order to get them all.  If you want to limit it, please use the StampCriteria version of 
	 * this call and turn paging off or set a limit.
	 * 
	 * @param id The id name of the user.
	 * 
	 * @return A list of Stamps, or null if the user doesn't exist.
	 * @throws GowallaException
	 */
	public List<Stamp> getStamps(final Id<User> id) throws GowallaException {
		return getStamps(new StampCriteria.Builder(id).build());
	}
	
	/**
	 * Get a User by id.  If no user is found by that id, null is returned.
	 * 
	 * @param identity The id of the user.
	 * @return A User object or null if not found.
	 * @throws GowallaException
	 */
	public FullUser getUser(final Id<User> identity) throws GowallaException {
		return getUser(identity.getId());
	}
	
	/**
	 * Get the currently authenticated user.
	 * 
	 * @return A User object or null if not found.
	 * @throws GowallaException
	 */
	public FullUser getUser() throws GowallaException {
		try {
			final String response = get("/users/me");
			return responseTranslator.translateUser(response);
		} catch(RequestNotAcceptableException e) {
			// No authenticated user.
			return null;
		} catch(RequestNotFoundException e) {
			// No authenticated user
			return null;			
		}
	}
	
	/**
	 * Get a User by id.  If no user is found by that id, null is returned.
	 * 
	 * @param id The integer id of the user.
	 * @return A User object or null if not found.
	 * @throws GowallaException
	 */
	public FullUser getUser(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d", id));
			return responseTranslator.translateUser(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}	
	}
	
	/**
	 * Get a User by login name.  If no user is found by that id, null is returned.
	 * 
	 * @param login The String login name of the user.
	 * @return A User object or null if not found.
	 * @throws GowallaException
	 */
	public FullUser getUser(final String login) throws GowallaException {
		try {
			final String response = get(String.format("/users/%s", login));
			return responseTranslator.translateUser(response);
		} catch(RequestNotAcceptableException e) {
			// No User.
			return null;
		} catch(RequestNotFoundException e) {
			// No User.
			return null;			
		}	
	}
	
	/**
	 * Get the top spots the given user has checked in to.  If the user is not found, null
	 * will be returned.
	 * 
	 * @param id The id of the user.
	 * @return A list of spots and the number of checkins.
	 * @throws GowallaException
	 */
	public List<VisitedSpot> getTopSpots(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d/top_spots", id));
			return responseTranslator.translateVisitedSpots(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}	
	}
	
	/**
	 * Get the top spots the given user has checked in to.  If the user is not found, null
	 * will be returned.
	 * 
	 * @param identity The id of the user.
	 * @return A list of spots and the number of checkins.
	 * @throws GowallaException
	 */
	public List<VisitedSpot> getTopSpots(final Id<User> identity) throws GowallaException {
		return getTopSpots(identity.getId());
	}
		
	/**
	 * Get a set of spot ids this user has visited.
	 * 
	 * @param identity The id of the user.
	 * @return A set of Spot Ids the user has visited.
	 * @throws GowallaException
	 */
	public Set<Id<Spot>> getVisitedSpotIds(final Id<User> identity) throws GowallaException {
		return getVisitedSpotIds(identity.getId());
	}
	
	/**
	 * Get a set of spot ids this user has visited.
	 * 
	 * @param id
	 * @return A set of Spot Ids the user has visited.
	 * @throws GowallaException
	 */
	public Set<Id<Spot>> getVisitedSpotIds(final int id) throws GowallaException {
		try {
			final String response = get(String.format("/users/%d/visited_spots_urls", id));
			return responseTranslator.translateVisitedSpotIds(response);
		} catch(RequestNotAcceptableException e) {
			// No User for this number.
			return null;
		} catch(RequestNotFoundException e) {
			// No User for this number.
			return null;			
		}
	}
	
	/**
	 * All-purpose paging.  
	 */
	private <T> List<T> pageRequests(final String resource, final PagedDataHandler<T> handler, final RequestParameter... params) throws GowallaRequestException, RateLimitExceededException {
		PagingInfo paging = null;
		int page = 1;
		final List<T> results = new LinkedList<T>();
		
		do {

			HashSet<RequestParameter> paramSet = new HashSet<RequestParameter>();
			Collections.addAll(paramSet, params);
			paramSet.add(new StringRequestParameter("page", page++));
			
			final String response = get(resource, paramSet);
			results.addAll(handler.translate(response));
			paging = getPagingInfo(response);
		} while(paging.hasMore() && 
				handler.canPage() && 
				(handler.maxResults() == 0 || handler.maxResults() > results.size()));
		if(handler.maxResults() > 0 && results.size() > handler.maxResults()) {
			return Lists.trim(results, handler.maxResults());
		}
		return results;
	}
	
	
	/**
	 * Encapsulate request handler and rate limitation call.
	 */
	private String request(final RequestMethod method, final String resource, Collection<RequestParameter> params) throws RateLimitExceededException, GowallaRequestException {
		rateLimitPreRequest();
		try {
			if(method == RequestMethod.GET) {
				return handler.getResource(resource, authentication, params);
			} else {
				return handler.postResource(resource, authentication, params);
			}
		} finally {
			rateLimitPostRequest();
		}
	}
	
	/**
	 * Encapsulate request handler and rate limitation call.
	 */
	protected String get(final String resource, RequestParameter... params) throws RateLimitExceededException, GowallaRequestException {
		return request(RequestMethod.GET, resource, Arrays.asList(params));
	}
	
	/**
	 * GET with collection.
	 */
	protected String get(final String resource, Collection<RequestParameter> params) throws RateLimitExceededException, GowallaRequestException {
		return request(RequestMethod.GET, resource, params);
	}
	
	/**
	 * Encapsulate request handler and rate limitation call.
	 */
	protected String post(final String resource, RequestParameter... params) throws RateLimitExceededException, GowallaRequestException {
		return request(RequestMethod.POST, resource, Arrays.asList(params));
	}
	
	/**
	 * Encapsulate request handler and rate limitation call.
	 */
	protected String post(final String resource, Collection<RequestParameter> params) throws RateLimitExceededException, GowallaRequestException {
		return request(RequestMethod.POST, resource, params);
	}
	/**
	 * Enforce the rate limitation.
	 * @throws RateLimitExceededException 
	 */
	private void rateLimitPreRequest() throws RateLimitExceededException {
		if(rateLimiter != null && !rateLimiter.preRequest()) {
			throw new RateLimitExceededException();
		}
	}
	
	/**
	 * Release the rate limitation.
	 * @throws RateLimitExceededException 
	 */
	private void rateLimitPostRequest() throws RateLimitExceededException {
		if(rateLimiter != null) {
			rateLimiter.postRequest();
		}
	}		
	
	/**
	 * Get the paging information from this response.
	 */
	protected PagingInfo getPagingInfo(final String response) {
		return responseTranslator.translatePagingInfo(response);
	}
}
